package com.ddd.core.exception;

import cn.hutool.core.util.StrUtil;
import com.ddd.core.api.ApiResult;
import com.ddd.core.constants.enums.ApiCodeEnum;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.web.servlet.error.AbstractErrorController;

import org.springframework.boot.web.servlet.error.ErrorAttributes;
import org.springframework.core.annotation.Order;
import org.springframework.http.HttpStatus;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.HttpMessageNotReadableException;
import org.springframework.validation.BindException;
import org.springframework.validation.FieldError;
import org.springframework.web.HttpRequestMethodNotSupportedException;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.MissingServletRequestParameterException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseStatus;
import org.springframework.web.bind.annotation.RestControllerAdvice;
import org.springframework.web.context.request.ServletWebRequest;
import org.springframework.web.method.annotation.MethodArgumentTypeMismatchException;

import javax.servlet.http.HttpServletRequest;
import javax.validation.ConstraintViolation;
import javax.validation.ConstraintViolationException;
import java.util.Set;
import java.util.stream.Collectors;

import static java.net.HttpURLConnection.*;
import static java.net.HttpURLConnection.HTTP_BAD_REQUEST;


/**
 * 异常处理器，用于处理默认常见的异常处理
 *
 * @author zhongwenbin
 * &#064;dateTime  2023/7/15 16:56
 */

@Slf4j
@RestControllerAdvice
@RequestMapping("${server.error.path:${error.path:/error}}")
public class ExceptionAdvice extends AbstractErrorController {

    private static final String METHOD_NOT_ALLOW_MESSAGE = " 希望请求类型是%s,实际请求类型是%s";
    private static final String PARAMETERS_TYPE_NOT_ALLOW_MESSAGE = "错误的请求参数: [%s]希望的参数类型为[%s]";
    private final ErrorAttributes errorAttributes;
    private final HttpServletRequest request;

    public ExceptionAdvice(ErrorAttributes errorAttributes, HttpServletRequest request) {
        super(errorAttributes);
        this.errorAttributes = errorAttributes;
        this.request = request;
    }


    /**
     * 其他错误-用于错误兜底
     *
     * @param e 错误
     * @return ApiResult<String>
     */
    @ExceptionHandler({Throwable.class})
    public ApiResult<String> handleThrowable(Throwable e) {
        log.error(e.getMessage(), e);
        return ApiResult.failed(ApiCodeEnum.FAILED);
    }

    /**
     * 用于处理未被捕捉的异常
     *
     * @param e 异常
     * @return ApiResult<String>
     */
    @ExceptionHandler({Exception.class})
    public ApiResult<String> handleException(Exception e) {
        log.error(e.getMessage(), e);
        return ApiResult.failed(ApiCodeEnum.FAILED);
    }

    /**
     * 当进行数据绑定过程时，可能会抛出 {@link BindException}。这个异常通常在以下情况下发生：
     * <p>1.当绑定失败时，会产生一个或多个绑定错误的情况，每个错误都会被封装为一个 FieldError 对象。</p>
     * <p>2.绑定错误可以是由于将用户输入的数据绑定到对象或表单对象时发生错误，或者由于验证过程中出现了错误。</p>
     * <p>3.BindException 是 Spring 框架中的一个特定异常，用于捕获和处理数据绑定过程中的错误。</p>
     * <p>可以通过调用 {@code getFieldErrors()} 方法来获取所有的绑定错误，然后对其进行处理。</p>
     *
     * @param e 参数绑定异常
     * @return ApiResult<String>
     * @see BindException
     */
    @ExceptionHandler(BindException.class)
    public ApiResult<String> handleBindException(BindException e) {
        try {
            FieldError fieldError = e.getFieldError();
            String defaultMessage = fieldError != null ? fieldError.getDefaultMessage() : null;
            if (fieldError != null && StrUtil.isNotBlank(defaultMessage) && fieldError.isBindingFailure()) {
                return ApiResult.failed(ApiCodeEnum.PARAMETER_ILLEGAL.getCode(), defaultMessage);
            }
        } catch (Exception ignored) {
        }
        // 参数类型不匹配检验
        StringBuilder str = new StringBuilder();
        e.getFieldErrors().forEach(v -> str.append("参数值:[").append(v.getField()).append("]的传入值:[")
                .append(v.getRejectedValue()).append("]与预期的值不匹配; "));
        return ApiResult.failed(ApiCodeEnum.PARAMETER_ILLEGAL.getCode(), str.toString());
    }

    /**
     * 当使用 Spring MVC 进行请求处理时，可能会抛出 {@link MethodArgumentNotValidException}。这个异常通常在以下情况下发生：
     * <p>1.当请求参数的验证失败时，会产生一个或多个不合法的方法参数的情况，每个不合法的参数都会被封装为一个 FieldError 对象。</p>
     * <p>2.不合法的方法参数可以是由于请求参数的值不满足验证注解中定义的约束条件，或者由于验证过程中出现了错误。</p>
     * <p>MethodArgumentNotValidException 是 Spring MVC 框架中的一个特定异常，用于捕获和处理请求参数验证失败的情况。</p>
     * <p>您可以通过调用 {@code getBindingResult().getFieldErrors()} 方法来获取所有的不合法参数错误，然后对其进行处理。</p>
     *
     * @param e 方法参数无效异常
     * @return ApiResult<String>
     * @see MethodArgumentNotValidException
     */
    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResult<String> handleMethodArgumentNotValidException(MethodArgumentNotValidException e) {
        FieldError fieldError = e.getBindingResult().getFieldError();
        String message = fieldError != null ? fieldError.getDefaultMessage() : "参数验证失败";
        return ApiResult.failed(ApiCodeEnum.PARAMETER_ILLEGAL.getCode(), message);
    }

    /**
     * 当使用 Bean Validation（如 Hibernate Validator）进行验证时，可能会抛出 {@link ConstraintViolationException}。这个异常通常在以下情况下发生：
     * <p>1.当验证失败时，会产生一个或多个约束违反的情况，每个违反都会被封装为一个 {@code ConstraintViolation} 对象。</p>
     * <p>2.约束违反可以是由于属性值不满足验证注解中定义的约束条件，或者由于验证过程中出现了错误。</p>
     * <p>ConstraintViolationException 是一个通用的异常，可以捕获并处理验证过程中的所有约束违反。</p>
     * <p>可以通过调用 {@code getConstraintViolations()} 方法来获取所有的约束违反，然后对其进行处理。</p>
     *
     * @param e 约束违反异常
     * @return ApiResult<String>
     * @see ConstraintViolationException
     */
    @ExceptionHandler({ConstraintViolationException.class})
    public ApiResult<String> handleConstraintViolationException(ConstraintViolationException e) {
        Set<ConstraintViolation<?>> constraintViolations = e.getConstraintViolations();
        String errorMessage = constraintViolations.isEmpty() ? "参数验证失败" :
                constraintViolations.stream()
                        .map(ConstraintViolation::getMessage)
                        .collect(Collectors.joining(";"));
        return ApiResult.failed(ApiCodeEnum.PARAMETER_ILLEGAL.getCode(), errorMessage);
    }

    /**
     * {@link MissingServletRequestParameterException} 用于表示缺少请求参数的异常情况。
     * 当请求参数缺失时，Spring MVC 会抛出这个异常。这个异常通常在以下情况下发生：
     * <p>1.当请求中缺少必需的参数时，例如在请求 URL 或请求体中缺少必需的参数。</p>
     * <p>2.如果您使用 {@code @RequestParam} 注解来声明方法参数，并且将参数设置为必需的（默认情况下是必需的），
     * 但是请求中没有提供该参数，那么就会抛出 MissingServletRequestParameterException 异常。</p>
     * <p>3.异常中会包含缺少的参数的名称和类型等信息，您可以通过捕获该异常并处理来提供自定义的错误响应或处理逻辑。</p>
     *
     * @param e 缺少Servlet请求参数异常
     * @return ApiResult<String>
     * @see MissingServletRequestParameterException
     */
    @ExceptionHandler(MissingServletRequestParameterException.class)
    public ApiResult<String> handleMissingServletRequestParameterException(MissingServletRequestParameterException e) {
        return ApiResult.failed(ApiCodeEnum.PARAMETER_ILLEGAL.getCode(), "缺少路径参数:" + e.getParameterName());
    }

    /**
     * {@link MethodArgumentTypeMismatchException}，它通常在方法参数类型不匹配的情况下抛出。
     * 这个异常通常发生在请求参数的类型与方法参数的类型不一致时。
     * <p>MethodArgumentTypeMismatchException 可能发生在以下情况下：</p>
     * <p>1.请求参数的类型与方法参数的类型不匹配，例如将字符串类型的参数传递给一个期望整数类型的方法参数。</p>
     * <p>请求参数的格式不正确，无法正确地转换为方法参数的类型，例如将一个不符合日期格式的字符串传递给一个期望日期类型的方法参数。</p>
     *
     * @param e 方法参数类型不匹配异常
     * @return ApiResult<String>
     * @see MethodArgumentTypeMismatchException
     */
    @ExceptionHandler(MethodArgumentTypeMismatchException.class)
    public ApiResult<String> handleMethodArgumentTypeMismatchException(MethodArgumentTypeMismatchException e) {
        String classType = e.getParameter().getParameterType().getSimpleName();
        String parameterName = e.getParameter().getParameterName();
        String str = String.format(PARAMETERS_TYPE_NOT_ALLOW_MESSAGE, parameterName, classType);
        return ApiResult.failed(ApiCodeEnum.PARAMETER_ILLEGAL.getCode(), str);
    }

    /**
     * HttpRequestMethodNotSupportedException 用于表示不支持的 HTTP 请求方法的异常情况。
     * 当客户端发送一个不被服务器支持的 HTTP 请求方法时，Spring MVC 会抛出这个异常。这个异常通常在以下情况下发生：
     * <p>1.当客户端发送一个不被服务器支持的 HTTP 请求方法时，例如使用了不在服务器允许的方法列表中的方法（如 PUT、DELETE 等）。</p>
     * <p>2.如果您在控制器方法上使用了 @RequestMapping 注解，并且指定了允许的请求方法列表（通过 method 参数），
     * 那么如果客户端发送了一个不在列表中的请求方法，就会抛出 HttpRequestMethodNotSupportedException 异常。</p>
     * <p>异常中会包含不支持的请求方法的名称和支持的请求方法列表等信息，您可以通过捕获该异常并处理来提供自定义的错误响应或处理逻辑。</p>
     *
     * @param e Http请求方法不支持异常
     * @return {@link ApiResult<String>}
     * @see HttpRequestMethodNotSupportedException
     */
    @ExceptionHandler(HttpRequestMethodNotSupportedException.class)
    public ApiResult<String> handleHttpRequestMethodNotSupportedException(HttpRequestMethodNotSupportedException e) {
        String method = e.getMethod();
        String[] supportedMethods = e.getSupportedMethods();
        String support = supportedMethods != null && supportedMethods.length > 0 ? supportedMethods[0] : "UNKNOWN";
        String format = String.format(METHOD_NOT_ALLOW_MESSAGE, support, method);
        return ApiResult.failed(ApiCodeEnum.METHOD_NOT_ALLOWED.getCode(), ApiCodeEnum.METHOD_NOT_ALLOWED.getMessage() + format);
    }


    /**
     * {@link HttpMessageNotReadableException} 表示无法读取 HTTP 消息的异常。
     * 当发生以下情况时，就会抛出 HttpMessageNotReadableException：
     * <p>1.请求体中的内容无法被解析或读取，可能是由于格式不正确或编码问题。</p>
     * <p>2.请求体中的 JSON 或 XML 格式不正确。</p>
     * <p>3.请求体中缺少必需的参数或字段。</p>
     * <p>4.请求体中的参数类型与目标对象不匹配。</p>
     * <p>总结：{@link HttpMessageConverter}在{@code HttpMessageConverter.read}方法失败时抛出</p>
     *
     * @param e Http消息不可读异常
     * @return ApiResult<String>
     * @see HttpMessageNotReadableException
     */
    @ExceptionHandler(HttpMessageNotReadableException.class)
    public ApiResult<String> handleHttpMessageNotReadableException(HttpMessageNotReadableException e) {
        String msg;
        Throwable cause = e.getMostSpecificCause();
        if (cause.getMessage() != null) {
            msg = cause.getMessage();
            // 可以截取前 200 字符，避免太长
            msg = msg.length() > 200 ? msg.substring(0, 200) + "..." : msg;
        } else {
            msg = "请求体格式错误或缺失必填字段";
        }
        return ApiResult.failed(ApiCodeEnum.PARAMETER_ILLEGAL.getCode(), msg);
    }

    /**
     * 用于重写SpringMVC的错误控制器，可以捕捉过滤器、拦截器所发生的异常。
     * <p>根据状态码判断返回</p>
     * <p>1.如果返回的{@link HttpStatus}状态码为：400、404、400-500将直接返回给客户端</p>
     * <p>如果是拦截器或者过滤器抛出的异常，那么交由{@code handleThrowable}方法进行处理</p>
     *
     * @return ApiResult<String>
     * @throws Throwable 异常
     */
    @RequestMapping
    @ResponseStatus(value = HttpStatus.OK)
    public ApiResult<String> handleError() throws Throwable {
        ServletWebRequest webRequest = new ServletWebRequest(request);
        //http响应码
        HttpStatus httpStatus = getStatus(request);
        int status = httpStatus.value();
        String statusStr = String.valueOf(status);
        //异常
        Throwable throwable = errorAttributes.getError(webRequest);
        if (status == HTTP_NOT_FOUND) {
            return ApiResult.failed(statusStr, "地址不存在");
        }
        if (status > HTTP_BAD_REQUEST && status < HTTP_INTERNAL_ERROR) {
            return ApiResult.failed(statusStr, "请求无效");
        }
        if (throwable == null) {
            return ApiResult.failed(statusStr, "请求无效");
        }
        String message = throwable.getMessage() != null ? throwable.getMessage() : "请求无效";
        if (status == HTTP_BAD_REQUEST) {
            return ApiResult.failed(statusStr, message);
        }
        // 交由 handleThrowable 处理
        throw throwable;
    }

}
